package com.yh.csx.crm.core.common;

import com.yh.csx.crm.core.entity.CommonResponse;
import com.yh.csx.crm.core.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * @author Huang Zhaoping
 * @date 2019/1/28
 */
@Slf4j
public class ExceptionHandler implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof BusinessException) {
                return handleBusinessException((BusinessException) ex, request, response, handler);
            }
            ModelAndView mv = handleSpringException(request, response, handler, ex);
            if (mv == null) {
                mv = handleCommonException(ex, request, response, handler);
            }
            return mv;
        } catch (Exception handlerException) {
            if (log.isWarnEnabled()) {
                log.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
            }
            return null;
        }
    }

    // 业务异常处理
    private ModelAndView handleBusinessException(BusinessException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request, ex.getCause());
        return createErrorView(request, response, handler, CommonResponse.error(ex));
    }

    // 通用异常处理
    private ModelAndView handleCommonException(Exception ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logError(ex, request, ex);
        return createErrorView(request, response, handler, "System error");
    }

    // Spring框架异常处理
    @SuppressWarnings("deprecation")
    private ModelAndView handleSpringException(HttpServletRequest request, HttpServletResponse response,
                                               Object handler, Exception ex) throws Exception {
        if (ex instanceof HttpRequestMethodNotSupportedException) {
            return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request,
                    response, handler);
        } else if (ex instanceof HttpMediaTypeNotSupportedException) {
            return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response,
                    handler);
        } else if (ex instanceof HttpMediaTypeNotAcceptableException) {
            return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response,
                    handler);
        } else if (ex instanceof MissingPathVariableException) {
            return handleMissingPathVariable((MissingPathVariableException) ex, request,
                    response, handler);
        } else if (ex instanceof MissingServletRequestParameterException) {
            return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request,
                    response, handler);
        } else if (ex instanceof ServletRequestBindingException) {
            return handleServletRequestBindingException((ServletRequestBindingException) ex, request, response,
                    handler);
        } else if (ex instanceof ConversionNotSupportedException) {
            return handleConversionNotSupported((ConversionNotSupportedException) ex, request, response, handler);
        } else if (ex instanceof TypeMismatchException) {
            return handleTypeMismatch((TypeMismatchException) ex, request, response, handler);
        } else if (ex instanceof HttpMessageNotReadableException) {
            return handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, request, response, handler);
        } else if (ex instanceof HttpMessageNotWritableException) {
            return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, request, response, handler);
        } else if (ex instanceof MethodArgumentNotValidException) {
            return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex, request, response,
                    handler);
        } else if (ex instanceof MissingServletRequestPartException) {
            return handleMissingServletRequestPartException((MissingServletRequestPartException) ex, request,
                    response, handler);
        } else if (ex instanceof BindException) {
            return handleBindException((BindException) ex, request, response, handler);
        } else if (ex instanceof NoHandlerFoundException) {
            return handleNoHandlerFoundException((NoHandlerFoundException) ex, request, response, handler);
        } else if (ex instanceof AsyncRequestTimeoutException) {
            return handleAsyncRequestTimeoutException(
                    (AsyncRequestTimeoutException) ex, request, response, handler);
        } else if (ex instanceof MultipartException) {
            return handleMultipartException(
                    (MultipartException) ex, request, response, handler);
        }
        return null;
    }

    private ModelAndView handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
                                                             HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        String[] supportedMethods = ex.getSupportedMethods();
        if (supportedMethods != null) {
            response.setHeader("Allow", StringUtils.arrayToDelimitedString(supportedMethods, ", "));
        }
        return createErrorView(request, response, handler, "Request method not supported");
    }

    private ModelAndView handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex,
                                                         HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            response.setHeader("Accept", MediaType.toString(mediaTypes));
        }
        return createErrorView(request, response, handler, "Media type not supported");
    }

    private ModelAndView handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex,
                                                          HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        return createErrorView(request, response, handler, "Media type not acceptable");
    }

    private ModelAndView handleMissingPathVariable(MissingPathVariableException ex,
                                                   HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Missing path variable");
    }

    private ModelAndView handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
                                                              HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Missing request parameter");
    }

    private ModelAndView handleServletRequestBindingException(ServletRequestBindingException ex,
                                                              HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Request Binding exception");
    }

    private ModelAndView handleConversionNotSupported(ConversionNotSupportedException ex,
                                                      HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Conversion not supported");
    }

    private ModelAndView handleTypeMismatch(TypeMismatchException ex,
                                            HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Type mismatch");
    }

    private ModelAndView handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
                                                      HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Http message not readable");
    }

    private ModelAndView handleHttpMessageNotWritable(HttpMessageNotWritableException ex,
                                                      HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Http message not writable");
    }

    private ModelAndView handleMethodArgumentNotValidException(MethodArgumentNotValidException ex,
                                                               HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        StringBuilder builder = new StringBuilder();
        ex.getBindingResult().getFieldErrors().forEach(error -> {
            String message = error.getDefaultMessage();
            if (builder.length() > 0) {
                builder.append('\n');
            }
            builder.append(message);
        });
        if (log.isDebugEnabled()) {
            log.debug("request[" + request.getRequestURI() + "] validate fail: " + builder.toString().replace("\n", " ,"));
        }
        return createErrorView(request, response, handler, "Method argument not valid: " + builder.toString());
    }

    private ModelAndView handleMissingServletRequestPartException(MissingServletRequestPartException ex,
                                                                  HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        return createErrorView(request, response, handler, "Missing request part");
    }

    private ModelAndView handleBindException(BindException ex, HttpServletRequest request,
                                             HttpServletResponse response, Object handler) throws IOException {
        StringBuilder builder = new StringBuilder();
        ex.getBindingResult().getFieldErrors().forEach(error -> {
            if (builder.length() > 0) {
                builder.append(", ");
            }
            builder.append(error.getField()).append("=").append(error.getRejectedValue());
        });
        if (log.isDebugEnabled()) {
            log.debug("request[" + request.getRequestURI() + "] bind fail: " + builder.toString());
        }
        return createErrorView(request, response, handler, "Bind exception：" + builder);
    }

    private ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
                                                       HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        return createErrorView(request, response, handler, "No handler found");
    }

    private ModelAndView handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex,
                                                            HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Async request timeout");
    }

    private ModelAndView handleMultipartException(MultipartException ex,
                                                  HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {

        logWarn(ex, request);
        return createErrorView(request, response, handler, "Multipart exception");
    }

    private ModelAndView createErrorView(HttpServletRequest request, HttpServletResponse response, Object handler, String message) throws IOException {
        return createErrorView(request, response, handler, CommonResponse.error(message));
    }

    // 创建结果视图
    private ModelAndView createErrorView(HttpServletRequest request, HttpServletResponse response, Object handler, CommonResponse<?> error) throws IOException {
        if (error != null) {
            MappingJackson2JsonView jackson = new MappingJackson2JsonView();
            jackson.setExtractValueFromSingleKeyModel(true);
            ModelAndView mv = new ModelAndView(jackson);
            mv.addObject(error);
            return mv;
        }
        if (!response.isCommitted()) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
        // default forward to /error view
        return new ModelAndView();
    }

    private void logWarn(Exception ex, HttpServletRequest request) {
        logWarn(ex, request, null);
    }

    private void logWarn(Exception ex, HttpServletRequest request, Throwable cause) {
        if (!log.isWarnEnabled()) {
            return;
        }
        String message = ex.getClass().getSimpleName() + "[" + request.getMethod() + " " + request.getRequestURI() + "]: " + ex.getMessage();
        if (cause == null) {
            log.warn(message);
        } else {
            log.warn(message, cause);
        }
    }

    private void logError(Exception ex, HttpServletRequest request, Throwable cause) {
        if (!log.isErrorEnabled()) {
            return;
        }
        String message = ex.getClass().getSimpleName() + "[" + request.getMethod() + " " + request.getRequestURI() + "]: " + ex.getMessage();
        if (cause == null) {
            log.error(message);
        } else {
            log.error(message, cause);
        }
    }
}
